package com.dynamic.mybatis.core.parser.xml;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.dynamic.mybatis.core.enums.CommandType;
import com.dynamic.mybatis.core.enums.ReturnType;
import com.dynamic.mybatis.core.exceptions.DmsDocumentException;
import com.dynamic.mybatis.core.generator.MapperMethod;
import com.dynamic.mybatis.core.metadata.DynamicConfiguration;
import com.dynamic.mybatis.core.metadata.DynamicMappedStatement;
import com.dynamic.mybatis.core.parser.NamespaceHelper;
import com.dynamic.mybatis.core.parser.field.DynamicMixedMappedStatementFieldParser;
import com.dynamic.mybatis.core.parser.xml.expand.MyDefaultElement;
import com.dynamic.mybatis.core.parser.xml.expand.MySAXReader;
import com.dynamic.mybatis.core.parser.xml.validator.DmsXMLMapperEntityResolver;
import com.dynamic.mybatis.core.parser.xml.validator.MybatisXmlValidator;
import com.dynamic.mybatis.core.session.DynamicSqlSession;
import com.dynamic.mybatis.core.toolkit.BeanUtils;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXParseException;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @Description
 * @Author xs
 * @Date 2021/11/10 15:48
 */
public abstract class DynamicBaseMappedStatementParser{
    protected Configuration configuration;
    protected DynamicSqlSession dynamicSqlSession;
    protected static final String trimBlankRegex =  "^\\s*|\\s*$";
    protected static final String[] statementXmlTags = "<select|<insert|<update|<delete|<parameterMap|<resultMap|<sql".split("\\|");

    protected static final String[] evalTags = Stream.of("update","insert","select","delete","reference").toArray(String[]::new);

    public DynamicBaseMappedStatementParser(DynamicSqlSession dynamicSqlSession) {
        this.dynamicSqlSession = dynamicSqlSession;
        this.configuration = dynamicSqlSession.getSqlSession().getConfiguration();
    }
    public  void parseMapperXmlBodyAddConfiguration(String mapperBodyXml,String namespace) {
        parseMapperXmlAddConfiguration(MybatisXmlValidator.buildXmlMapperBody(mapperBodyXml,namespace));
    }

    public  void parseMapperXmlAddConfiguration(String xml) {
        DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
        ByteArrayInputStream byteArrayInputStream  = new ByteArrayInputStream(xml.getBytes());
        Map<String, XNode> sqlFragments = new HashMap<>();
        XMLMapperBuilder xmlMapperBuilder = createXMLMapperBuilder(byteArrayInputStream,configuration, dynamicConfiguration.DYNAMIC_RESOURCE,sqlFragments);
        xmlMapperBuilder.parse();
        dynamicConfiguration.addSqlFragments(sqlFragments);
    }

    protected List<ResultMap> parserResultMap(String id, String resultType, String resultMap){
        List<ResultMap> resultMaps = new ArrayList<>();
        if(StringUtils.isNotBlank(resultType)){
            Class  resultTypeClass =  configuration.getTypeAliasRegistry().resolveAlias(resultType);
            ResultMap resultMapObj = new ResultMap.Builder(configuration,id + "-Inline",resultTypeClass, new ArrayList<>(),null).build();
            resultMaps.add(resultMapObj);
        }else if(StringUtils.isNotBlank(resultMap)){
            XNode xNode  =  new XPathParser(resultMap).evalNode("/resultMap");
            id =  xNode.getStringAttribute("id");
            DynamicMappedStatement dms = new DynamicMappedStatement();
            dms.setId(id);
            dms.setSqlStatement(resultMap);
            parseDynamicMappedStatement(dms);
            resultMaps.add(configuration.getResultMap(id));
        }
        return resultMaps;
    }

    public DynamicMappedStatement parseDynamicMappedStatement(DynamicMappedStatement dms){
        try {
            //解析动态新xml SQL
            if(isXml(trimBlank(dms.getSqlStatement()))){
                DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
                String  xml = MybatisXmlValidator.buildXmlMapperBody(dms.getSqlStatement(),dms.getDynamicMappedStatementNamespace());
                ByteArrayInputStream byteArrayInputStream  = new ByteArrayInputStream(xml.getBytes());
                Map<String, XNode> sqlFragments = new HashMap<>();
                XMLMapperBuilder xmlMapperBuilder = createXMLMapperBuilder(byteArrayInputStream,configuration, dynamicConfiguration.DYNAMIC_RESOURCE,sqlFragments);
                xmlMapperBuilder.parse();
                dynamicConfiguration.addSqlFragments(sqlFragments);

                dynamicConfiguration.addDynamicMappedStatement(dms);
            }else {
                //SQL解析
                SqlSource sqlSource = createSqlSource(configuration,trimBlank(dms.getSqlStatement()),null);
                //初始化mappedStatement
                MappedStatement.Builder builder =  new MappedStatement.Builder(configuration,dms.getId(),sqlSource, CommandType.getSqlCommandType(dms.getCommandType()));
                builder.resultMaps(parserResultMap(dms.getId(),dms.getResultType(),dms.getResultMap()));
                MappedStatement mappedStatement = builder.build();
                //将mappedStatement 加入mybatis上下文中
                configuration.addMappedStatement(mappedStatement);

                ((DynamicConfiguration)configuration).addDynamicMappedStatement(dms);
            }
            //解析字段
            DynamicMixedMappedStatementFieldParser fieldParser =  dynamicSqlSession.getDynamicMappedStatementHelper().getMixedMappedStatementFieldParser();
            fieldParser.parseMappedStatementField(dms);

        }catch (Exception e){
            throw  ExceptionFactory.wrapException("mappedStatement parser is error or unknow type. mappedStatement is " + dms.getId() +" .Cause: "+e, e);
        }
        DynamicConfiguration dc =  ((DynamicConfiguration)configuration);
        if(dms.getCommandType() == null){
            MappedStatement mappedStatement =   dc.getMappedStatement(dms.getId());
            dms.toBuilder().setCommandType(mappedStatement.getSqlCommandType());
        }
        if(!dc.hasMappedStatement(dms.getId())){
            dc.addDynamicMappedStatement(dms);
        }
        return dms;
    }


    public XMLMapperBuilder createXMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
        try {
            XPathParser xPathParser =  new XPathParser(inputStream, true, configuration.getVariables(), new DmsXMLMapperEntityResolver());
            Constructor constructor =  XMLMapperBuilder.class.getDeclaredConstructor(XPathParser.class,Configuration.class,String.class,Map.class);
            constructor.setAccessible(true);
            return   (XMLMapperBuilder)constructor.newInstance(xPathParser,configuration,resource,sqlFragments);
        } catch (Exception e) {
           throw  new RuntimeException(e);
        }
      //  return  new XMLMapperBuilder(inputStream,configuration, resource, sqlFragments);
    }


    protected List<XNode>  parser(String script){
        List<XNode> xNodes = new ArrayList<>();
        for(String tag : statementXmlTags){
            XNode xNode  =  createXPathParser(script).evalNode(tag);
            xNodes.add(xNode);
        }
        return xNodes;
    }

    public String validateXmlMapperBody(String xmlMapperBody,String namespace) throws SAXParseException {
        String xml = MybatisXmlValidator.buildXmlMapperBody(xmlMapperBody,namespace);
        MybatisXmlValidator.validate(xml,3);
        return xml;
    }

    protected XPathParser createXPathParser(String xml){
        ByteArrayInputStream byteArrayInputStream  = new ByteArrayInputStream(xml.getBytes());
        return new XPathParser(byteArrayInputStream, true, configuration.getVariables(), new DmsXMLMapperEntityResolver());
    }

    public List<DynamicMappedStatement> document4jParser(String xml){
        Document document =  createDom4jParser(xml);
        Element mapperElement = document.getRootElement();

        String namespace = mapperElement.attributeValue("namespace");
        List<Element> elements = evalNodes(mapperElement,evalTags);
        List<DynamicMappedStatement> dmsList = new ArrayList<>();
        DynamicConfiguration dynamicConfiguration = (DynamicConfiguration) configuration;
        for(Element element : elements){
            String shortId = element.attributeValue("id");
            DynamicMappedStatement dms;
            if(element.getName().equals("reference")){

                String referenceId = element.attributeValue("referenceId");
                DynamicMappedStatement referenceDms =  dynamicConfiguration.getDynamicMappedStatement(referenceId);
                if(referenceDms == null){
                    String message = " does not contain value for " + referenceId;
                    if(element instanceof MyDefaultElement){
                        MyDefaultElement myDefaultElement = (MyDefaultElement)element;
                        throw new DmsDocumentException(message,myDefaultElement.getLineNumber(),myDefaultElement.getColumnNumber());
                    }else {
                        throw new IllegalArgumentException(message);
                    }
                }
                String dmsId;
                if(StringUtils.isBlank(shortId)){
                    dms = referenceDms;
                    dmsId = NamespaceHelper.getDynamicMappedStatementId(namespace,dms.getShortId());
                }else {
                    dms =  BeanUtils.serializeCopy(referenceDms);
                    dms = dms.toBuilder().setId(shortId).setNamespace(namespace).setReferenceId(referenceDms.getId()).builder();
                    dynamicConfiguration.addDynamicMappedStatement(dms);
                    dmsId = dms.getId();
                }
                MappedStatement ms  = dynamicConfiguration.getMappedStatement(dmsId);
                if(ms == null){
                    //生成新的MappedStatement
                    Element ele =  getElementById(dms.getSqlStatement(),referenceDms.getShortId());
                    ele.attribute("id").setValue(dms.getShortId());
                    String elementXml = ele.asXML();
                    this.parseMapperXmlBodyAddConfiguration(elementXml,namespace);
                }

            }else {
                String id = NamespaceHelper.getDynamicMappedStatementId(namespace,shortId);
                ReturnType returnType = ReturnType.getReturnType(element.attributeValue("returnType"));
                MapperMethod mapperMethod = MapperMethod.get(shortId);
                returnType = returnType == null && mapperMethod != null ? ReturnType.getReturnType(mapperMethod.getReturnType().name()) : returnType;
                String method = CommandType.StandardCommand.SELECT.name().equalsIgnoreCase(element.getName()) ? "GET" : "POST";
                DynamicMappedStatement.Builder dmsBuilder =   new DynamicMappedStatement().toBuilder()
                        .setId(id)
                        .setNamespace(NamespaceHelper.getNamespace(namespace))
                        .setSqlStatement(element.asXML())
                        .setCommandType(element.getName())
                        .setParameterWrapper(element.attributeValue("parameterWrapper"))
                        .setResultWrapper(element.attributeValue("resultWrapper"))
                        .setName(element.attributeValue("name"))
                        .setBatchExecute(Boolean.valueOf(element.attributeValue("batchExecute")))
                        .setBatchSize(Integer.valueOf(Objects.toString(element.attributeValue("batchSize"),"0")))
                        .setResultType(element.attributeValue("resultType"))
                        .setResultMap(element.attributeValue("resultMap"))
                        .setReturnType(returnType);
                dmsBuilder.setMethod(method);
                dms = dmsBuilder.builder();
            }
            dmsList.add(dms);
            mapperElement.remove(element);
            dms.setAuth(Boolean.TRUE);
        }
        return dmsList;
    }

    public Element parserDom4jMapperBodyXml(String  mapperBodyXml,String namespace) {
        String xml =  MybatisXmlValidator.buildXmlMapperBody(mapperBodyXml,namespace);
       return createDom4jParser(xml).getRootElement();
    }


    protected Document createDom4jParser(String xml){
        ByteArrayInputStream byteArrayInputStream  = new ByteArrayInputStream(xml.getBytes());
        SAXReader saxReader = new MySAXReader();
        saxReader.setEntityResolver(new DmsXMLMapperEntityResolver());
        try {
            return saxReader.read(byteArrayInputStream);
        } catch (DocumentException e) {
            throw new DmsDocumentException(e);
        }
    }

    protected String evalIgnoreNodes(Element element){
        return evalIgnoreNodes(element,evalTags);
    }

    protected String evalIgnoreNodes(Element element ,String ...ignoreTagNames){
        List<Element>  elements = evalNodes(element,ignoreTagNames);
        elements.stream().forEach(e-> element.remove(e));
        return Objects.toString(element.elements().stream().map(e-> ((Element)e).asXML()).collect(Collectors.joining(System.lineSeparator())),"");
    }
    protected  List<Element> evalNodes(Element element , String... tagNames){
        List<Element> elementList= element.elements();
        if(elementList == null){
            return Collections.EMPTY_LIST;
        }
        List<String> tags =  Arrays.stream(tagNames).collect(Collectors.toList());
        return  elementList.stream().filter(e -> tags.contains(e.getName())).collect(Collectors.toList());
    }

    /**
     * @Description: 初始化sql对象
     * @param configuration mybatis上下文
     * @param mappedStatement 动态SQL
     * @param parameterType 参数类型
    　*/
    protected SqlSource createSqlSource(Configuration configuration, String mappedStatement, String parameterType){
        Class parameterTypeClass;
        if(com.dynamic.mybatis.core.toolkit.StringUtils.isBlank(parameterType)){
            parameterTypeClass = HashMap.class;
        }else {
            parameterTypeClass = configuration.getTypeAliasRegistry().resolveAlias(parameterType);
        }
        return configuration.getDefaultScriptingLanguageInstance().createSqlSource(configuration,mappedStatement,parameterTypeClass);
    }


    public Element getElementById(String  mapperBodyXml,String id){
        Element element =  parserDom4jMapperBodyXml(mapperBodyXml,"test");
        for(Element e : (List<Element>) element.elements()){
            if(Objects.equals(e.attributeValue("id"),id)){
                return  e;
            }
        }
        return null;
    }

    protected String trimBlank(String str){
        if(str == null){
            return str;
        }
        Pattern pattern = Pattern.compile(trimBlankRegex);
        Matcher matcher = pattern.matcher(str);
        return  matcher.replaceAll("");
    }


    public static boolean isXml(String str) {
        for(String tag : statementXmlTags){
            if(str.startsWith(tag)){
                return true;
            }
        }
        return false;
    }
}